Java Virtual Machine Technology (opens new window)

Java 虚拟机

  • 编译器负责生成字节码
  • JIT 编译器负责优化成本地代码
  • JVM 解释器负责输出期望结果
  • 字节码优化技术:内联 inlining、消除 elimination、标量化 scalarization

# JVM 内存结构

虚拟机在执行 Java 程序的过程中会把所管理的内存划分为若干个不同的数据区域:

JVM内存结构

  • 方法区:与堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等数据
    • HotSpot 虚拟机把方法区叫做永久代(Permanent Generation)
    • 在 jdk1.7 中,符号引用放到 native heap,字符串常量类的静态变量放到 java heap
    • 在 jdk1.8 中,永久代被移除,类的元数据放到本地堆内存(native heap)中,这一块区域叫 Metaspace(元空间)
  • 运行时常量池:方法区的一部分,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
  • :被所有线程共享的一块内存区域,在虚拟机启动时创建。所有的对象实例以及数组都要在堆上分配内存
    • 使用 new 关键字,就表示在堆中开辟一块新的存储空间
    • 堆内存又被划分成不同的部分:伊甸区(Eden)、幸存者区域(Survivor Sapce)、老年代(Old Generation Space)
    • 堆内存由新生代/年轻代和老年代组成,而新生代又被分成三部分,Eden 空间、From Survivor 空间、To Survivor 空间,默认情况下新生代按照 8:1:1 的比例来分配
    • 伊甸区(Eden),对象被创建的时候首先放到这个区域,进行垃圾回收后,不能被回收的对象被放入到空的 To Survivor 空间
  • Java 虚拟机栈每个线程创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,每个方法被执行时都会同时创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息;当方法调用完毕,该方法的栈帧就被销毁了
  • 本地方法栈:每个线程在调用本地方法时都会创建一个栈帧,为虚拟机使用到的 native 方法服务
  • 程序计数器:每个线程都有自己的程序计数器,用来记录当前线程正在执行的字节码指令的地址

Java Memory Model

# JVM 常见参数

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

JVM内存各区域内存大小的控制参数

  • -Xms 或 -XX:InitialHeapSize 设置堆的初始空间大小,默认为物理内存的 1/64,默认单位为 字节,可用单位:k 或 K、m 或 M、g 或 G
  • -Xmx 或 -XX:MaxHeapSize 设置堆的最大空间大小,默认为物理内存的 1/4
  • -Xss 或 -XX:ThreadStackSize 设置每个线程的栈大小,jdk1.5 以后默认为 1M
  • -Xdebug 在调试模式下运行
  • -Xmn 设置新生代初始空间和最大空间大小
  • -XX:NewSize 设置新生代初始空间大小
  • -XX:MaxNewSize 设置新生代最大空间大小
  • -XX:PermSize 设置永久代初始空间大小
  • -XX:MaxPermSize 设置永久代最大空间大小
  • -XX:MetaspaceSize 设置元空间初始空间大小
  • -XX:MaxMetaspaceSize 设置元空间最大空间大小
  • -XX:NewRatio 设置老年代与新生代的比值,默认值为 2(新生代占 1,老年代占2,即新生代占整个堆的 1/3)
  • -XX:SurvivorRatio 设置新生代中 Eden 区与 2 个 Survivor 区的比值
  • -XX:MaxTenuringThreshold 表示一个对象如果在 Survivor 区移动的次数达到设置值还没有被垃圾回收就进入老年代(如果设置为 0,则新生代对象不经过 Survivor 区,直接进入老年代)(The default value is 15 for the parallel (throughput) collector, and 6 for the CMS collector.)
  • -XX:PretenureSizeThreshold 直接晋升到老年代的对象大小,默认为 0
  • -XX:+PrintCommandLineFlags 输出 JVM 的启动参数
  • -XX:+PrintGC 输出 GC 日志
  • -XX:+PrintGCDetails 输出详细的 GC 处理日志
  • -XX:+PrintGCDateStamps 输出 GC 发生时的日期戳
  • -XX:+PrintGCTimeStamps 输出 GC 发生时 JVM 的启动时间
  • -XX:+PrintHeapAtGC 输出 GC 前后的堆内存情况
  • -XX:+PrintGCApplicationStoppedTime 输出应用由于 GC 而产生的停顿时间
  • -Xloggc:gc.log 输出 GC 日志到文件
  • -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 输出 safepoint 信息,日志会输出到 JVM 进程的标准输出(GC 调试)
  • -XX:+HeapDumpOnOutOfMemoryError 当 JVM 出现 OOM 时生成一个 HPROF 格式的堆转储文件(默认文件名 java_pid<进程号>.hprof
  • -XX:HeapDumpPath=/home/app/jvmheap

http://jvmmemory.com/

最大线程数量 =(机器本身可用内存 - JVM 分配的堆内存)/ Xss 值

为了减少 JVM 垃圾回收和重新分配内存的频率可以把 Xms 和 Xmx 设置同样的值

-Xmx2g XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=. -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+PrintGCApplicationStoppedTime -Xloggc:gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M

注意设置 JVM 参数(VM options)的方式:java [-options] -jar jarfile [args...]

打印 VM 参数:ManagementFactory.getRuntimeMXBean().getInputArguments();

记录 Java GC 日志: 对于 Java 4、5、6、7、8,-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:<file-path>;对于 Java 9,-Xlog:gc*:file=<file-path>

# GC 算法

# 对象存活判断

判断对象是否存活一般有两种方式:

  1. 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加 1,引用释放时计数减 1,计数为 0 时可以回收(Java 并没有选择引用计数,因为存在对象相互循环引用的问题)
  2. 可达性分析(Reachability Analysis):从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则认为此对象是不可达的

可作为 GC Roots 的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象,例如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
  • 本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象
  • 在方法区中类静态属性引用的对象,例如 Java 类的引用类型静态变量
  • 在方法区中常量引用的对象,例如字符串常量池里的引用
  • Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器所有被同步锁(synchronized 关键字)持有的对象
  • 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等

Types of GC roots (opens new window)

  • JNI global reference
  • JNI local reference
  • Local variable on stack - 本地方法栈中的局部变量。这些变量通常存在于栈中,引用对象则存在于堆中。
  • Monitor - 用作同步监视器的对象。
  • System class - 由启动类加载器(Bootstrap ClassLoader)加载的类。这些类通常是 Java 标准库的一部分,并在 JVM 启动时加载。
  • Thread - 活动的 Java 线程(已启动但未停止的线程)。运行中的线程无法被收集。线程对象及其堆栈可以通过其线程局部变量保存对其他对象的引用。
  • Other - JVM 出于其目的而从垃圾收集中保留的对象(与 JVM 的实现有关)。可能的已知情况有:系统类加载器、一些 JVM 知道的重要的异常类、一些用于处理异常的预分配对象、以及在加载类的过程中的自定义类加载器等。

Implementing Finalization (opens new window)

A reachable object is any object that can be accessed in any potential continuing computation from any live thread.

Optimizing transformations of a program can be designed that reduce the number of objects that are reachable to be less than those which would naively be considered reachable. For example, a Java compiler or code generator may choose to set a variable or parameter that will no longer be used to null to cause the storage for such an object to be potentially reclaimable sooner.

可达对象(reachable object)是可以从任何活动线程的任何潜在的持续访问中的任何对象。java 编译器或代码生成器可能会对不再访问的对象提前置为 null,使得对象可以被提前回收。

class A {
    @Override protected void finalize() {
        System.out.println(this + " was finalized!");
    }

    public static void main(String[] args) throws InterruptedException {
        A a = new A();
        System.out.println("Created " + a);
        for (int i = 0; i < 1_000_000_000; i++) {
            if (i % 1_000_00 == 0)
                System.gc();
        }
        System.out.println("done.");
    }
}

// 打印结果
// 方法没有执行完,栈帧并没有出栈,但是对象 a 被提前回收
Created A@1be6f5c3
A@1be6f5c3 was finalized! //finalize 方法输出
done.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 垃圾回收机制

  • 自动垃圾回收机制;只能回收堆内存中不再被程序引用的对象所占的内存
  • 回收内容:堆中的可回收对象;方法区无用的元数据(卸载不再使用的类型,默认 -XX:+ClassUnloadingWithConcurrentMark)

# 引用

  1. 强引用(Strong Reference):最常见的一种,只要该引用存在,就不会被 GC
  2. 软引用(Soft Reference):内存空间不足时,进行回收
  3. 弱引用(Weak Reference):当 JVM 进行 GC 时,则进行回收,无论内存是否充足
  4. 虚引用(Phantom Reference):不会对其生存时间构成影响,其 PhantomReference#get() 方法总是返回 null,因此无法获取被引用的对象,主要用于跟踪一个对象被回收的过程

# 垃圾收集算法

  • 垃圾收集算法主要有:复制、标记-清除、标记-整理
  1. 复制算法(Copying)

    • 将内存分为大小相等的两块,每次只用其中一块,一块内存用完之后,将存活对象复制到另一块内存区域,然后将原来的半块内存区城全部回收
    • 只需移动堆顶指针,按顺序分配内存即可,实现简单,运行高效,且内存回收后不会产生内存碎片
    • 缺点:可用内存缩小为原来的一半,代价高
  2. 标记-清除算法(Mark-Sweep)

    • 首先标记出所有需要回收的对象,然后统一回收被标记的对象
    • 缺点:标记和清除过程的效率都不高;内存回收后会产生大量不连续的内存碎片
  3. 标记-整理算法(Mark-Compact)

    • 标记需要回收的对象,将存活对象移动到一端,然后将端边界以外的内存回收
  4. 分代收集算法(Generational Collection)

    • GC 分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短
    • 把 Java 堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法
    • 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;而老年代中因为对象存活率高、没有额外空间对它进行分配担保,必须使用“标记-清理”或“标记-整理”算法来进行回收

# 垃圾收集过程

  • 当 Eden 区的空间占用达到一定阈值时,触发 Minor GC(又称 Young GC):
    Eden 区中所有存活的对象都会被复制到 To 区域,而在 From 区中仍存活的对象会根据它们的年龄值来决定去向,年龄达到一定值(年龄阈值)的对象会被移动到老年代中,没有达到阈值的对象会被复制到 To 区域
  • Major GC/Old GC:指目标只是老年代的垃圾收集,目前只有 CMS 收集器会有单独收集老年代的行为
  • 对整个堆和方法区进行的清理叫作 Full GC,触发条件:手动调用 System.gc()、老年代或方法区空间不足
    • 年轻代全部对象大小 > 老年代剩余空间
    • 从年轻代存活超过 15 次后进入老年代的对象大小 > 老年代剩余空间
    • Survivor 区中不足以容纳年轻代中存活下来的对象,此时这部分的对象大小 > 老年代剩余空间
    • 如果是 CMS 收集器,老年代剩余空间 < 参数比例

# 垃圾收集器 Garbage Collector

Java HotSpot Virtual Machine Garbage Collection Tuning Guide (opens new window)

  1. Serial 收集器:一个采用单个线程并基于复制算法工作在新生代的收集器,进行垃圾收集时,必须暂停其它所有的工作线程(Stop The World),是 Client 模式下 JVM 的默认选项,-XX:+UseSerialGC
  2. ParNew 收集器:Serial 收集器的多线程版本(使用多个线程进行垃圾收集),-XX:+UseParNewGC,是启用 CMS 收集器时新生代的默认收集器
  3. Parallel Scavenge 收集器:一个采用多线程基于复制算法并工作在新生代的收集器,也被称作是吞吐量优先的 GC,是早期 jdk1.8 等版本中 Server 模式 JVM 的默认 GC 选择,-XX:+UseParallelGC,使用 Parallel Scavenge(新生代)+ Serial Old(老年代)的组合进行 GC
  4. Serial Old 收集器:一个采用单线程基于标记-整理算法并工作在老年代的收集器
  5. CMS 收集器(Concurrent Mark Sweep):一种以尽量减少停顿时间为目标的收集器,工作在老年代,基于标记-清除算法实现,-XX:+UseConcMarkSweepGC,CMS 的执行过程可以分为以下几个阶段:初始标记(STW)→并发标记→并发预清理→重标记(STW)→并发清理→重置
  6. Parallel Old 收集器:一个采用多线程基于标记-整理算法并工作在老年代的收集器,适用于注重于吞吐量及 CPU 资源敏感的场合,-XX:+UseParallelOldGC,使用 Parallel Scavenge(新生代)+ Parallel Old(老年代)的组合进行 GC
  7. G1 收集器:jdk1.7 提供的一个工作在新生代和老年代的收集器,基于标记-整理算法实现,在收集结束后可以避免内存碎片问题,一种兼顾吞吐量和停顿时间的 GC,是 Oracle JDK 9 以后的默认 GC 选项

常用的垃圾收集器组合

垃圾收集器对比与应用场景

  • jdk1.7 默认垃圾收集器 Parallel Scavenge(新生代)+ Parallel Old(老年代)
  • jdk1.8 默认垃圾收集器 Parallel Scavenge(新生代)+ Parallel Old(老年代)
  • jdk1.9 默认垃圾收集器 G1

java -XX:+PrintCommandLineFlags -version 查看 JVM 使用的 XX 选项的默认值

JDK-6679764 : enable parallel compaction by default (opens new window):在 jdk7u4 之前 -XX:+UserParallelGC 使用的是 Parallel Scavenge + Serial Old,在这个版本后 Parallel Old GC 已经很成熟了,所以直接替换了旧的收集器,所以 jdk7u4 后的 JDK7 和 JDK8 默认使用的都是 Parallel Scavenge + Parallel Old

jdk1.8_GC

  • GC 调优思路:
    1. 降低 GC 频率:可以通过增大堆空间,减少不必要对象生成
    2. 降低 GC 暂停时间:可以通过减少堆空间,使用 CMS GC 算法实现
    3. 避免 Full GC:调整 CMS 触发比例,避免 Promotion Failure 和 Concurrent mode failure(老年代分配更多空间,增加 GC 线程数加快回收速度),减少大对象生成等

GC 日志含义

# JVM 性能监控与故障诊断工具

JDK Tools and Utilities (opens new window)

# JDK 监控和故障诊断命令行工具

  • jps -l:JVM 进程状态工具,列出系统上的 JVM 进程
  • jcmd:JVM 命令行调试工具,用于向 JVM 进程发送调试命令
  • jstat:JVM 统计监控工具,附加到一个 JVM 进程上收集和记录 JVM 的各种性能指标数据
    • jstat -gcutil <pid> 5000 100:输出 GC 和内存占用汇总信息,每隔 5 秒输出一次,输出 100 次(其中,S0 表示 Survivor0 区占用百分比,S1 表示 Survivor1 区占用百分比,E 表示 Eden 区占用百分比,O 表示老年代占用百分比,M 表示元数据区占用百分比,YGC 表示新生代回收次数,YGCT 表示新生代回收耗时,FGC 表示老年代回收次数,FGCT 表示老年代回收耗时)
  • jstack:JVM 栈查看工具,可以打印 JVM 进程的线程栈和锁情况
  • jinfo:JVM 信息查看工具,查看 JVM 的各种配置信息
  • jmap:JVM 堆内存分析工具,可以打印 VM 进程对象直方图、类加载统计,以及做堆转储操作
    • jmap -dump:format=b,file=/tmp/a.hprof <pid>:生成虚拟机的堆内存转储快照(heapdump 文件)
    • jmap -heap <pid>:显示堆详细信息,包括使用的 GC 算法、堆配置信息和各内存区域内存使用信息
    • jmap -histo:live <pid>:显示堆中对象的统计信息,包括每个 Java 类的对象数量(只计算活动的对象)、内存大小
  • jhat:JVM Heap Dump Browser,用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果
# 定位问题线程信息
jps  # 查看 Java 进程的进程 id(21711)
top -Hp 21711  # 查看进程的线程信息
# 输入 P 将线程按照 CPU 使用率排序
printf "%x\n" 21742  # 21742 对应的 16 进制值
jstack 21711 | grep 54ee -A100 > dump.log  # 查看线程快照(注意:执行该命令的用户需是该进程的运行用户)
# 使用 python 开启一个简单 http 服务
python -m SimpleHTTPServer 8080
curl -o http://ip地址:8080/dump.log
1
2
3
4
5
6
7
8
9
  • 线程快照中线程状态有:
    1. 死锁,Deadlock(重点关注)
    2. 执行中,Runnable
    3. 等待资源,Waiting on condition(重点关注)
    4. 等待获取监视器,Waiting on monitor entry(重点关注)
    5. 暂停,Suspended
    6. 对象等待中,Object.wait() 或 TIMED_WAITING
    7. 阻塞,Blocked(重点关注)
    8. 停止,Parked
  • 在线分析工具:fastThread (opens new window)(Java Thread Dump Analyzer)、GCeasy (opens new window)(Universal GC Log Analyzer)、HeapHero (opens new window)(Java Heap Dump Analyzer)

# JDK 中的可视化工具

  • jvisualvm:综合的 JVM 监控工具,查看 JVM 基本情况、做栈和堆转储、做内存和 CPU profiling 等(可安装 Visual GC 插件)
  • jconsole:JMX 兼容的图形工具,用于监控 JVM 基本情况,查看 MBean
  • jmc(Java Mission Control)

# 其它监控分析工具

使用 MAT 分析 OOM 问题,一般可以按照以下思路进行:

  • 通过支配树功能或直方图功能查看消耗内存最大的类型,来分析内存泄露的大概原因
  • 查看那些消耗内存最大的类型、详细的对象明细列表,以及它们的引用链,来定位内存泄露的具体点
  • 配合查看对象属性的功能,可以脱离源码看到对象的各种属性的值和依赖关系,帮助我们理清程序逻辑和参数
  • 辅助使用查看线程栈来看 OOM 问题是否和过多线程有关,甚至可以在线程栈看到 OOM 最后一刻出现异常的线程

# Java 内存模型(JMM)

Java 内存模型(Java Memory Model,JMM)是一个抽象的概念,描述了一组规则或规范,定义了程序中各个共享变量访问规则

CPU与内存

  • 每个 CPU 都有自己的高速缓存,所有 CPU 共享同一个主内存

Java内存模型

  • JMM 规定了所有的变量都存储在主内存(Main Memory)中
  • 每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile 变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)
  • 不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成

# Java 对象模型

  • Java 对象在 JVM 中的存储结构/数据结构
  • HotSpot 虚拟机中,设计了一个 OOP-Klass Model,OOP(Ordinary Object Pointer)指的是普通对象指针,而 Klass 用来描述对象实例的具体类型
  • 每一个 Java 类,在被 JVM 加载的时候,JVM 会给这个类创建一个 instanceKlass,保存在方法区,用来在 JVM 层表示该 Java 类。当在 Java 代码中使用 new 创建一个对象的时候,JVM 会创建一个 instanceOopDesc 对象,这个对象中包含了对象头以及实例数据

Java对象模型

Java对象的数据结构

  • 在 HotSpot 虚拟机中,Java 对象在内存中存储的布局可以分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
  1. 对象头
    • Mark Word,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,其中的最后 2bit 是锁状态标志位(无锁、偏向锁、轻量级锁、重量级锁、GC 标识)
    • 类型指针(元数据指针),即对象指向它的类元数据的指针,JVM 通过该指针来确定这个对象是哪个类的实例
    • 数组长度,如果对象是一个数组,在对象头中还有一块数据用于记录数组长度
  2. 实例数据
    • 实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
    • 类变量所占用的空间通常不算在对象本身,因为其引用是在方法区
    • 基本类型所占内存大小:byte(1b)、short(2b)、int(4b)、long(8b)、float(4b)、double(8b)、char(2b)、boolean(1b)
  3. 对齐填充
    • 由于 HotSpot JVM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,即对象的大小必须是 8b 的整数倍,当对象的对象头+实例数据不是 8 的倍数时,需要通过对齐填充来补全
  • Java 对象所占内存大小
    1. 一般非数组对象:12b 对象头(8b Mark Word + 4/8b 元数据指针)+ 数据区 + padding 内存对齐(按照 8 的倍数对齐)
    2. 数组对象:16/20b 对象头(8b Mark Word + 4/8b 元数据指针 + 4b 数组长度)+ 数据区 + padding 内存对齐(按照 8 的倍数对齐)
    3. Oracle JDK 从 6 update 23 开始在 64 位系统、堆内存小于 32GB 的情况下会默认开启压缩指针 UseCompressedOops,将原来 64 位的指针压缩为 32 位,即此时指针为 4b
  • 获取对象所占内存字节数: ObjectSizeCalculator.getObjectSize(obj)
Updated at: 2024-06-01 17:10:24